package it.antonio.ilpizze.db.io;


import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.lang.reflect.Method;
import java.nio.ByteBuffer;
import java.nio.MappedByteBuffer;
import java.nio.channels.ClosedChannelException;
import java.nio.channels.FileChannel;
import java.nio.channels.FileLock;
import java.nio.channels.OverlappingFileLockException;
import java.nio.channels.FileChannel.MapMode;

public class IOMMapFile extends IOFile {
	
	private MappedByteBuffer buffer;
	private int resize_bytes;
	private boolean synch = true;
	
	public static final int RESIZE_SIZE = 10000;
	
	private File file;
	
	private RandomAccessFile accessFile;
	protected FileChannel channel;
	
	private boolean open = false;
	
	private FileLock fileLock;
	
	
	
	private static Method cleanerMethod;
	static {
		try {
			final Class<?> sunClass = Class.forName("sun.nio.ch.DirectBuffer");
			cleanerMethod = sunClass.getMethod("cleaner");
		} catch (Exception e) {

		}
	}

	
	
	private static final int OPEN_RETRY = 10;
	private static final int OPEN_DELAY = 100;
	
	private static final int NUMBER_OF_TRIES = 1000;
	
	protected IOMMapFile(File file, int resize_bytes) {
		this.file = file;
		this.resize_bytes = resize_bytes;
	}

	
	
	@Override
	public void open() {

		if (!file.exists() || file.length() == 0) {
			create();
		}
		openChannel();
		
		try {
			buffer = channel.map(MapMode.READ_WRITE, 0, channel.size());
		} catch (IOException e) {
			throw new IOFileException(e);
		}	
		
		open = true;
	}
	
	private void openChannel() {
		for (int i = 0; i < OPEN_RETRY; ++i) {
			try {
				accessFile = new RandomAccessFile(file, "rw");
				channel = accessFile.getChannel();
				
				return;
			} catch (FileNotFoundException e) {
				if (i == NUMBER_OF_TRIES) {
					throw new IOFileException(e);
				}
				try {
					Thread.sleep(OPEN_DELAY);
				} catch (InterruptedException e1) {
					Thread.currentThread().interrupt();
				}

			}
		}
		
		throw new IOFileException("Cannot open the file");
	}

	private void create() {
		try {
			File parent = file.getParentFile();
			if (parent != null && !parent.exists()) {
				parent.mkdirs();
			}
			file.createNewFile();
		} catch (IOException e) {
			throw new IOFileException(e);
		}
	}

	protected void checkOpen() {
		if (!open) {
			throw new IOFileException("Open the file first");
		}
	}
	
	@Override
	public void close() {
		checkOpen();
		
		synch();
		clean(buffer);
		
		try {

			channel.close();
			channel = null;
			accessFile.close();
			accessFile = null;
			open = false;
		} catch (IOException e) {
			throw new IOFileException(e);
		}
	}


	@Override
	public int size() {
		checkOpen();
		try {
			return (int) channel.size();	
		} catch (IOException e) {
			throw new IOFileException(e);
		}
	}

	
	
	@Override
	public boolean lock(int time) {
		checkOpen();
		if(fileLock != null){
			throw new IOFileException("File already locked. Unlock it first!");
		}
		int ms = 0;
		while(true) {
			try {
				if(ms > time) return false;
				fileLock = channel.tryLock(0, 1, false);
				if(fileLock != null){
					return true;		
				}
				try {
					Thread.sleep(50);
					ms+=50;
				} catch (InterruptedException ie) {
				}
			} catch (OverlappingFileLockException e) {	
				System.gc();
				try {
					Thread.sleep(50);
					ms+=50;
				} catch (InterruptedException ie) {
				}
			} catch (IOException e) {
				throw new IOFileException(e);
			}
			
		}
	}
	
	@Override
	public void unlock() {
		checkOpen();
		synch();
		try {
			fileLock.release();
			fileLock = null;
		} catch (ClosedChannelException e) {
		} catch (IOException e) {
			throw new IOFileException(e);
		}
	}
	
	@Override
	public boolean isLocked(){
		return fileLock != null && fileLock.isValid();
	}
	
	
	@Override
	public boolean delete(){
		return this.file.delete();
	}
	
	@Override
	public File original() {
		return this.file;
	}
	
	
	
	@Override
	public long position() {
		checkOpen();
		return buffer.position();
	}

	@Override
	public void position(long position) {
		checkOpen();
		resizeIfOverflow((int)position - buffer.position());
		deSynch();
		buffer.position((int) position);
	}

	@Override
	public byte[] readBytes(int size) {
		checkOpen();
		resizeIfOverflow(size);
		deSynch();
		byte[] bytes = new byte[size];
		buffer.get(bytes);
		return bytes;
	}
	
	@Override
	public ByteBuffer readByteBuffer(int size) {
		checkOpen();
		resizeIfOverflow(size);
		deSynch();
		byte[] bytes = new byte[size];
		buffer.get(bytes);
		return ByteBuffer.wrap(bytes);
	}
	
	@Override
	public String readString() {
		checkOpen();
		resizeIfOverflow(SIZE_INT);
		deSynch();
		int size = buffer.getInt();
		resizeIfOverflow(size);
		byte[] bytes = new byte[size];
		buffer.get(bytes);
		return new String(bytes);
	}

	@Override
	public int readInt() {
		checkOpen();
		resizeIfOverflow(SIZE_INT);
		deSynch();
		return buffer.getInt();
	}

	@Override
	public boolean readBoolean() {
		checkOpen();
		resizeIfOverflow(SIZE_BYTE);
		deSynch();
		byte boolByte = buffer.get();
		if(boolByte == BOOLEAN_TRUE_BYTE){
			return true;
		} 
		if(boolByte == BOOLEAN_FALSE_BYTE){
			return false;
		}
		throw new IOFileException("Invalid boolean value");
	}

	@Override
	public long readLong() {
		checkOpen();
		resizeIfOverflow(SIZE_LONG);
		deSynch();
		return buffer.getLong();
	}

	@Override
	public short readShort() {
		checkOpen();
		resizeIfOverflow(SIZE_SHORT);
		deSynch();
		return buffer.getShort();
	}

	@Override
	public float readFloat() {
		checkOpen();
		resizeIfOverflow(SIZE_FLOAT);
		deSynch();
		return buffer.getFloat();
	}

	@Override
	public double readDouble() {
		checkOpen();
		resizeIfOverflow(SIZE_DOUBLE);
		deSynch();
		return buffer.getDouble();
	}

	@Override
	public byte readByte() {
		checkOpen();
		resizeIfOverflow(SIZE_BYTE);
		deSynch();
		return buffer.get();
	}
	
	private void resizeIfOverflow(int size) {
		try{
			int position = buffer.position(); 
			if(position + size > buffer.capacity()){
				channel.position(channel.size()+resize_bytes - 2);
				channel.write(ByteBuffer.allocate(1));	
				synch();
				clean(buffer);
				buffer = channel.map(MapMode.READ_WRITE, 0, channel.size());	
				buffer.position(position);
			}
			
		} catch (IOException e) {
			throw new IOFileException(e);
		}
		
	}

	@Override
	public void writeBytes(byte[] bytes) {
		checkOpen();
		resizeIfOverflow(bytes.length);
		deSynch();
		buffer.put(bytes);
	}
	@Override
	public void writeByteBuffer(ByteBuffer buffer) {
		checkOpen();
		resizeIfOverflow(buffer.capacity());
		deSynch();
		this.buffer.put(buffer);
	}
	
	@Override
	public void writeString(String value) {
		checkOpen();
		int length = value.length();
		resizeIfOverflow(SIZE_INT + length);
		deSynch();
		buffer.putInt(length);
		buffer.put(value.getBytes());
	}

	@Override
	public void writeInt(int value) {
		checkOpen();
		resizeIfOverflow(SIZE_INT);
		deSynch();
		buffer.putInt(value);
	}

	@Override
	public void writeBoolean(boolean value) {
		checkOpen();
		resizeIfOverflow(SIZE_BYTE);
		deSynch();
		buffer.put(value ? BOOLEAN_TRUE_BYTE : BOOLEAN_FALSE_BYTE);
	}

	@Override
	public void writeLong(long value) {
		checkOpen();
		resizeIfOverflow(SIZE_LONG);
		deSynch();
		buffer.putLong(value);
	}

	@Override
	public void writeShort(short value) {
		checkOpen();
		resizeIfOverflow(SIZE_SHORT);
		deSynch();
		buffer.putShort(value);
	}

	@Override
	public void writeFloat(float value) {
		checkOpen();
		resizeIfOverflow(SIZE_FLOAT);
		deSynch();
		buffer.putFloat(value);
	}

	@Override
	public void writeDouble(double value) {
		checkOpen();
		resizeIfOverflow(SIZE_DOUBLE);
		deSynch();
		buffer.putDouble(value);
	}

	@Override
	public void writeByte(byte value) {
		checkOpen();
		deSynch();
		resizeIfOverflow(SIZE_BYTE);
		buffer.put(value);
	}

	private void deSynch() {
		synch = false;	
	}

	private void clean(MappedByteBuffer buffer){
		if (cleanerMethod != null) {
			try {
				final Object cleaner = cleanerMethod.invoke(buffer);
				if (cleaner != null)
					cleaner.getClass().getMethod("clean").invoke(cleaner);
			} catch (Exception e) {
				
			}
		}
		
	}

	@Override
	public void synch() {
		if(!synch){
			buffer.force();
			synch = true;
		}
	}



	@Override
	public void renameTo(String newName) {
		file.renameTo(new File(file.getParentFile(), newName));
	}
	
	@Override
	public String name() {
		return file.getName();
	}
	
}
